package com.imooc.coupon.service.impl;

import com.alibaba.fastjson.JSON;
import com.imooc.coupon.constant.Constant;
import com.imooc.coupon.constant.CouponStatus;
import com.imooc.coupon.entity.Coupon;
import com.imooc.coupon.exception.CouponException;
import com.imooc.coupon.service.IRedisService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;


import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @Date 2021/11/2 19:48
 * @Author wangshbv
 * @Description redis的实现类
 */
@Slf4j
@Service
public class RedisServiceImpl implements IRedisService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public List<Coupon> getCachedCoupons(Long userId, Integer status) {
        log.info("get Coupons from Cache :[]", userId, JSON.toJSON(status));
        String redisKey = status2RedisKey(status, userId);
        List<String> couponStr = redisTemplate.opsForHash()
                .values(redisKey)
                .stream()
                .map(o -> Objects.toString(o, null)).collect(Collectors.toList());
        if (CollectionUtils.isEmpty(couponStr)) {
            saveEmptyCouponListToCache(userId, Collections.singletonList(status));
            return Collections.emptyList();
        }
        return couponStr.stream()
                .map(cs -> JSON.parseObject(cs, Coupon.class))
                .collect(Collectors.toList());
    }

    /**
     * 避免缓存穿透
     *
     * @author wangshbv
     * @date 20:13
     */
    @Override
    public void saveEmptyCouponListToCache(Long userId, List<Integer> status) {
        log.info("save Expty List To Cache :[]", userId, JSON.toJSON(status));
        Map<String, String> invalidCouponMap = new HashMap<>(16);
        invalidCouponMap.put("-1", JSON.toJSONString(Coupon.invalidCoupon()));
        //使用SessionCallBack 的pipeline
        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations)
                    throws DataAccessException {
                status.forEach(s -> {
                    String redisKey = status2RedisKey(s, userId);
                    operations.opsForHash().putAll(redisKey, invalidCouponMap);
                });
                return null;
            }
        };
        log.info("Pipeline Exe Result :{}", JSON.toJSONString(redisTemplate.executePipelined(sessionCallback)));
    }

    /**
     * 尝试从redis中获取一个优惠劵码
     *
     * @param templeId
     * @return
     */
    @Override
    public String tryToAcquireCouponFromCache(Integer templeId) {
        String redisKey = String.format("%s%s", Constant.RedisPrefix.COUPON_TEMPLATE, templeId.toString());
        //因为优惠劵不存在先后顺序的关系，左边pop和右边pop没有区别
        String couponCode = redisTemplate.opsForList().leftPop(redisKey);
        log.info("Acquire Coupon code:{},{},{}", templeId, redisKey, couponCode);
        return couponCode;
    }

    /**
     * 将优惠劵放在cache中
     *
     * @param userId
     * @param coupons
     * @param status
     * @return
     * @throws CouponException
     */
    @Override
    public Integer addCouponToCache(Long userId, List<Coupon> coupons,
                                    Integer status) throws CouponException {
        log.info("add coupon to cache :{},{},{}", userId, JSON.toJSONString(coupons), status);
        CouponStatus couponStatus = CouponStatus.of(status);
        Integer result = -1;
        switch (couponStatus) {
            case USEABLE:
                result = addCouponToCacheForUsable(userId, coupons);
                break;
            case USED:
                result = addCouponToCacheForUsed(userId, coupons);
                break;
            case EXPIRED:
                result = addCouponToCacheForExpired(userId, coupons);
                break;
        }
        return null;
    }

    private Integer addCouponToCacheForUsable(Long userId, List<Coupon> coupons) {
        //如果status是USABLE, 代表的是新增加的优惠卷
        log.debug("add coupon to cache for usable");
        Map<String, String> needCachedObject = new HashMap<>(coupons.size());
        coupons.forEach(c -> {
            needCachedObject.put(c.getId().toString(), JSON.toJSONString(c));
        });
        String redisKey = status2RedisKey(CouponStatus.USEABLE.getCode(), userId);
        redisTemplate.opsForHash().putAll(redisKey, needCachedObject);
        log.info("add {} : coupon to cache for usable", needCachedObject.size(), userId, redisKey);
        redisTemplate.expire(redisKey, getRandomExpirationTime(1, 2), TimeUnit.MILLISECONDS);
        return needCachedObject.size();
    }

    /**
     * 将已经使用的优惠卷加入到cache中
     *
     * @author wangshbv
     * @date 16:49
     */
    private Integer addCouponToCacheForUsed(Long userId, List<Coupon> coupons) throws CouponException {
        //如果status 是used,代表用户操作是使用当前的优惠卷，影响到两个Cache
        //usable used
        log.debug("add coupon to cache for usabled");
        Map<String, String> needCachedForUsed = new HashMap<>(coupons.size());
        String redisKeyUsable = status2RedisKey(CouponStatus.USEABLE.getCode(), userId);
        String redisKeyUsabled = status2RedisKey(CouponStatus.USED.getCode(), userId);
        //获取当前用户可用的优惠卷
        List<Coupon> curcachedCoupons = getCachedCoupons(userId, CouponStatus.USEABLE.getCode());
        assert curcachedCoupons.size() > coupons.size();
        coupons.forEach(c -> {
            needCachedForUsed.put(c.getId().toString(), JSON.toJSONString(c));
        });
        //检验当前的优惠卷参数是否与cached中的匹配
        List<Integer> curUsableIds = curcachedCoupons.stream().map(Coupon::getId).collect(Collectors.toList());
        List<Integer> paramIds = coupons.stream().map(Coupon::getId).collect(Collectors.toList());
        if (!CollectionUtils.isSubCollection(paramIds, curUsableIds)) {
            log.error("curCoupons is not equal cache", userId,
                    JSON.toJSONString(curUsableIds), JSON.toJSONString(paramIds));
            throw new CouponException("curCoupons is not equal cache");
        }
        //校验
        List<String> needCleanKey = paramIds.stream().map(o -> o.toString()).collect(Collectors.toList());
        SessionCallback<Objects> sessionCallback = new SessionCallback<Objects>() {
            @Override
            public Objects execute(RedisOperations operations) throws DataAccessException {
                //1.已使用的优惠券
                operations.opsForHash().putAll(redisKeyUsabled, needCachedForUsed);
                //2.可用的优惠券cache 需要清理
                operations.opsForHash().delete(redisKeyUsable, needCleanKey.toArray());
                //3,重置过期时间
                operations.expire(redisKeyUsable, getRandomExpirationTime(1, 2), TimeUnit.SECONDS);
                operations.expire(redisKeyUsabled, getRandomExpirationTime(1, 2), TimeUnit.SECONDS);
                return null;
            }
        };
        log.info("Pipeline Exe Result :{}", JSON.toJSONString(redisTemplate.executePipelined(sessionCallback)));
        return coupons.size();
    }

    /**
     * 将过期的优惠卷加入到缓存中
     *
     * @author wangshbv
     * @date 17:10
     */
    private Integer addCouponToCacheForExpired(Long userId, List<Coupon> coupons) throws CouponException {
        //status 是Expired影响到两个cache
        log.debug("add coupon to cache for Expired");
        Map<String, String> needCachedForExpired = new HashMap<>(coupons.size());
        String redisKeyUsable = status2RedisKey(CouponStatus.USEABLE.getCode(), userId);
        String redisKeyExpired = status2RedisKey(CouponStatus.EXPIRED.getCode(), userId);
        List<Coupon> curUserableCoupons = getCachedCoupons(userId, CouponStatus.USEABLE.getCode());
        List<Coupon> curExpiredCoupons = getCachedCoupons(userId, CouponStatus.EXPIRED.getCode());
        assert curUserableCoupons.size() > curExpiredCoupons.size();
        coupons.forEach(c -> {
            needCachedForExpired.put(c.getId().toString(), JSON.toJSONString(c));
        });
        //校验当前优惠卷中的参数是否跟cached中的匹配
        List<Integer> curUsableIds = curUserableCoupons.stream().map(Coupon::getId).collect(Collectors.toList());
        List<Integer> paramIds = coupons.stream().map(Coupon::getId).collect(Collectors.toList());
        if (!CollectionUtils.isSubCollection(paramIds, curUsableIds)) {
            log.error("curCoupons is not equal cache", userId,
                    JSON.toJSONString(curUsableIds), JSON.toJSONString(paramIds));
            throw new CouponException("curCoupons is not equal cache");
        }
        List<String> needCleanKey = paramIds.stream().map(i -> i.toString()).collect(Collectors.toList());
        SessionCallback<Objects> sessionCallback = new SessionCallback<Objects>() {
            @Override
            public Objects execute(RedisOperations operations) throws DataAccessException {
                //1.即将过期的优惠卷
                redisTemplate.opsForHash().putAll(redisKeyExpired, needCachedForExpired);
                //2.可用的优惠卷需要清理
                redisTemplate.opsForHash().delete(redisKeyUsable, needCleanKey.toArray());
                //3.重置过期时间
                operations.expire(redisKeyUsable, getRandomExpirationTime(1, 2), TimeUnit.SECONDS);
                operations.expire(redisKeyExpired, getRandomExpirationTime(1, 2), TimeUnit.SECONDS);
                return null;
            }
        };
        log.info("Pipeline Exe Result :{}", JSON.toJSONString(redisTemplate.executePipelined(sessionCallback)));
        return coupons.size();
    }

    /**
     * 根据status 获取对应的redis key
     *
     * @author wangshbv
     * @date 19:59
     */
    private String status2RedisKey(Integer status, Long userId) {
        String redisKey = null;
        CouponStatus couponStatus = CouponStatus.of(status);
        switch (couponStatus) {
            case USEABLE:
                redisKey = String.format("%s%s", Constant.RedisPrefix.USER_COUPON_USERABLE, userId);
                break;
            case USED:
                redisKey = String.format("%s%s", Constant.RedisPrefix.USER_COUPON_USED, userId);
                break;
            case EXPIRED:
                redisKey = String.format("%s%s", Constant.RedisPrefix.USER_COUPON_EXPIRE, userId);
                break;
        }
        return redisKey;
    }

    /**
     * 获取一个随机的过期时间
     * 缓存雪崩： key在同一时间失效，导致大量请求去访问数据库
     * 返回min 和max 之间的一个随机秒数
     *
     * @return
     */
    private Long getRandomExpirationTime(Integer min, Integer max) {
        return RandomUtils.nextLong(min * 60 * 60, max * 60 * 60);
    }
}
